package com.nulldev.util.web.YouTubeExtractor.subtitles.types;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Sets.DataSet;
import com.nulldev.util.data.legacy.XML.lXML;
import com.nulldev.util.data.legacy.XML.lXML.Attr;
import com.nulldev.util.data.legacy.XML.lXML.Doc;
import com.nulldev.util.data.legacy.XML.lXML.E;
import com.nulldev.util.data.legacy.XML.lXML.Item;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.YouTubeExtractor.subtitles.Subtitle;
import com.nulldev.util.web.YouTubeExtractor.subtitles.Subtitle.SubtitleStyle;

public class SRV3SubtitleParser {
	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--Subtitles.debug"));

	public static class WindowStyle {
		public int id = -1;
		/**
		 * Justify/Text Align: <br>
		 * Values: <br>
		 * 0 - Start (Left for LTR) <br>
		 * 1 - End (Right for RTL) <br>
		 * 2 - Center <br>
		 * 3 - Justify <br>
		 */
		public int ju = -1;
		/**
		 * Print Direction: <br>
		 * Values: <br>
		 * 0 - LTR, Horizontal <br>
		 * 1 - RTL, Horizontal <br>
		 * 2 - Vertical RTL, Upright Text <br>
		 * 3 - Vertical LTR, Sideways Text
		 */
		public int pd = -1;
		/**
		 * Scroll Direction: <br>
		 * Values: <br>
		 * 0 - LTR <br>
		 * 1 - RTL (REQUIRES VERTICAL PD)
		 */
		public int sd = -1;
		/**
		 * Mode Hint: <br>
		 * Values: <br>
		 * 0|1 = Default <br>
		 * 2 = Scrolling
		 */
		public int mh = -1;
		/**
		 * Window Fill Color
		 */
		public int wfc = -1;
		/**
		 * Window Fill Opacity
		 */
		public int wfo = -1;
	}

	public static class WindowPosition {
		public int id = -1;
		/**
		 * Anchor Point: <br>
		 * Values: <br>
		 * 0 = Top Left <br>
		 * 1 = Top Center <br>
		 * 2 = Top Right <br>
		 * 3 = Center Left <br>
		 * 4 = Center <br>
		 * 5 = Center Right <br>
		 * 6 = Bottom Left <br>
		 * 7 = Bottom Center <br>
		 * 8 = Bottom Right
		 */
		public int ap = -1;
		/**
		 * Column Count
		 */
		public int cc = -1;
		/**
		 * Row Count
		 */
		public int rc = -1;
		/**
		 * Align Horizontal (X from left)
		 */
		public int ah = -1;
		/**
		 * Align Vertical (X from top)
		 */
		public int av = -1;
	}

	public static class SRV3Pen {
		public int id = -1;
		public int bold = -1;
		public int italic = -1;
		public int underline = -1;
		public int fontStyle = -1;
		public int fontColor = -1;
		public int backgroundColor = -1;
		public int edgeColor = -1;
		public double sizeIncrement = -1;
		public int foregroundOpacity = -1;
		public int backgroundOpacity = -1;
	}

	private static DataSet<Integer, Integer> _make_entry_style(final E entry, final Map<Integer, WindowPosition> wps, final Map<Integer, WindowStyle> wss,
			final List<SubtitleStyle> styles) {
		final SubtitleStyle lPen = new SubtitleStyle();
		boolean shouldAddStyle = false;
		int posX = -1, posY = -1;
		if (entry.hasAttr("wp")) {
			final int wpi = Integer.parseInt(entry.get("wp"));
			final WindowPosition wp = wps.get(wpi);
			if (wp != null) {
				posX = wp.ah;
				posY = wp.av;
				shouldAddStyle = true;
			}
		}
		if (entry.hasAttr("ws")) {
			final int wsi = Integer.parseInt(entry.get("ws"));
			final WindowStyle ws = wss.get(wsi);
			if (ws != null) {
				lPen.color = ws.wfc;
				lPen.foregroundAlpha = ws.wfo;
				shouldAddStyle = true;
			}
		}
		if (shouldAddStyle)
			styles.add(lPen);
		return new DataSet<Integer, Integer>(posX, posY);
	}

	public static List<Subtitle> getSubtitles(final String data) {
		if (data == null)
			throw new NullPointerException("getSubtitles() -> data == null!");
		final List<Subtitle> out = FastLists.list(32);
		final Doc o = lXML.doc(data);
		final E timedtext = o.root();
		if (timedtext.getAttr("format") == null || !timedtext.get("format").equalsIgnoreCase("3"))
			throw new RuntimeException("Cannot handle the following format version: " + timedtext.get("format"));
		final E head = (E) timedtext.child("head");
		final Map<Integer, WindowPosition> wps = FastMaps.map();
		final Map<Integer, WindowStyle> wss = FastMaps.map();
		final Map<Integer, SRV3Pen> pens = FastMaps.map();
		if (head != null) {
			for (final Item _entry : head.children("wp")) { // Window Position
				final E entry = (E) _entry;
				final WindowPosition wp = new WindowPosition();
				for (final Attr attr : entry.attributes()) {
					switch (attr.name()) {
						case "ap": {
							wp.ap = Integer.parseInt(attr.value());
							break;
						}
						case "av": {
							wp.av = Integer.parseInt(attr.value());
							break;
						}
						case "ah": {
							wp.ah = Integer.parseInt(attr.value());
							break;
						}
						case "cc": {
							wp.cc = Integer.parseInt(attr.value());
							break;
						}
						case "rc": {
							wp.rc = Integer.parseInt(attr.value());
							break;
						}
						case "id": {
							wp.id = Integer.parseInt(attr.value());
							break;
						}
						default: {
							log.warn("Unknown wp attribute: " + attr);
							break;
						}
					}
				}
				wps.put(wp.id, wp);
			}
			for (final Item _entry : head.children("ws")) { // Window Styles
				final E entry = (E) _entry;
				final WindowStyle ws = new WindowStyle();
				for (final Attr attr : entry.attributes()) {
					switch (attr.name()) {
						case "ju": {
							ws.ju = Integer.parseInt(attr.value());
							break;
						}
						case "pd": {
							ws.pd = Integer.parseInt(attr.value());
							break;
						}
						case "sd": {
							ws.sd = Integer.parseInt(attr.value());
							break;
						}
						case "mh": {
							ws.mh = Integer.parseInt(attr.value());
							break;
						}
						case "wfc": {
							ws.wfc = Integer.parseInt(attr.value().replace("#", ""), 16);
							break;
						}
						case "wfo": {
							ws.wfo = Integer.parseInt(attr.value().replace("#", ""), 16);
							break;
						}
						case "id": {
							ws.id = Integer.parseInt(attr.value());
							break;
						}
						default: {
							log.warn("Unknown ws attribute: " + attr);
							break;
						}
					}
				}
				wss.put(ws.id, ws);
			}
			for (final Item _entry : head.children("pen")) { // Pen
				final E entry = (E) _entry;
				final SRV3Pen pen = new SRV3Pen();
				for (final Attr attr : entry.attributes()) {
					switch (attr.name()) {
						case "id": {
							pen.id = Integer.parseInt(attr.value());
							break;
						}
						case "b": {
							pen.bold = Integer.parseInt(attr.value());
							break;
						}
						case "i": {
							pen.italic = Integer.parseInt(attr.value());
							break;
						}
						case "u": {
							pen.underline = Integer.parseInt(attr.value());
							break;
						}
						case "fs": {
							pen.fontStyle = Integer.parseInt(attr.value());
							break;
						}
						case "fc": {
							pen.fontColor = Integer.parseInt(attr.value().replace("#", ""), 16);
							break;
						}
						case "bc": {
							pen.backgroundColor = Integer.parseInt(attr.value().replace("#", ""), 16);
							break;
						}
						case "ec": {
							pen.edgeColor = Integer.parseInt(attr.value().replace("#", ""), 16);
							break;
						}
						case "sz": {
							pen.sizeIncrement = Double.parseDouble(attr.value());
							break;
						}
						case "fo": {
							pen.foregroundOpacity = Integer.parseInt(attr.value());
							break;
						}
						case "bo": {
							pen.backgroundOpacity = Integer.parseInt(attr.value());
							break;
						}
						case "et": {
							// Skip for now
							break;
						}
						default: {
							log.warn("Unknown pen attribute: " + attr);
							break;
						}
					}
				}
				pens.put(pen.id, pen);
			}
		}
		final E body = (E) timedtext.child("body");
		if (body != null) {
			for (final Item _entry : body.children("p")) {
				final List<SubtitleStyle> styleList = new ArrayList<SubtitleStyle>();
				final E entry = (E) _entry;
				final StringBuilder text = new StringBuilder(64);
				try {
					final List<E> sList = entry.children("s");
					if (!sList.isEmpty()) {
						final StringBuilder b = new StringBuilder(32);
						sList.forEach((__entry) -> {
							b.append(formatText(__entry, __entry.text(), pens, styleList));
						});
						text.append(b.toString());
					} else
						text.append(formatText(entry, entry.text(), pens, styleList));
				} catch (Exception f) {
					log.error("Failed to get subtitle text!", f);
					continue;
				}
				final long _startTime = MathUtil.safeParseL(entry.get("t"), -1);
				if (_startTime == -1) {
					log.error("Failed to get startTime for: " + entry);
					continue;
				}
				final long _duration = MathUtil.safeParseL(entry.get("d"), -1);
				if (_duration == -1) {
					log.error("Failed to get duration for: " + entry);
					continue;
				}
				final DataSet<Integer, Integer> subStyle = _make_entry_style(entry, wps, wss, styleList);
				out.add(new Subtitle(text.toString(), _startTime, _duration, styleList, true).setPositionH(subStyle.getA()).setPositionV(subStyle.getB()));
			}
		}
		return out;
	}

	private static String formatText(final E entry, String text, final Map<Integer, SRV3Pen> pens, final List<SubtitleStyle> styles) {
		final int penID = MathUtil.safeParseInt(entry.get("p"), -1);
		if (penID <= -1 && !pens.containsKey(penID))
			return text;
		final SRV3Pen pen = pens.get(penID);
		final StringBuilder sb = new StringBuilder(text);
		final SubtitleStyle style = new SubtitleStyle();
		boolean shouldAppendStyle = false;
		if (pen.italic > 0) {
			sb.insert(0, "<i>");
			sb.append("</i>");
		}
		if (pen.bold > 0) {
			sb.insert(0, "<b>");
			sb.append("</b>");
		}
		if (pen.underline > 0) {
			sb.insert(0, "<u>");
			sb.append("</u>");
		}
		if (pen.fontColor > -1) {
			shouldAppendStyle = true;
			style.color = pen.fontColor;
			sb.insert(0, "<c." + style.textIdf + ">");
			sb.append("</c>");
		}
		if (pen.backgroundColor > -1) {
			final int color = pen.backgroundColor;
			if (color == 0) {
				style.backgroundAlpha = 0;
				shouldAppendStyle = true;
			} else
				log.warn("Unsupported property: 'backgroundColor' (value: " + color + ")");
		}
		if (pen.edgeColor > -1) {
			shouldAppendStyle = true;
			style.edgeColor = pen.edgeColor;
		}
		if (pen.fontStyle > -1) {
			shouldAppendStyle = true;
			style.fontFamily = pen.fontStyle;
		}
		if (pen.foregroundOpacity > -1) {
			shouldAppendStyle = true;
			style.foregroundAlpha = MathUtil.clamp(0, 254, pen.foregroundOpacity);
		}
		if (pen.backgroundOpacity > -1) {
			final int alpha = pen.backgroundOpacity;
			if (alpha >= 254 || alpha == 0) {
				style.backgroundAlpha = MathUtil.clamp(0, 254, alpha);
				shouldAppendStyle = true;
			} else
				log.warn("Unsupported background alpha: " + alpha);
		}
		if (shouldAppendStyle)
			styles.add(style);
		return sb.toString();
	}
}
